function Spreadsheet() {
    var prefs = new gadgets.Prefs(),
	//defaultLayout = "http://risegadgets.googlecode.com/svn/test/GoogleSpreadsheetCurrent/Table/Table.xml";
	defaultLayout = "http://c560290.r90.cf2.rackcdn.com/Table/Table.xml";
    this.prefs = prefs;
    this.url = prefs.getString("url");
    this.useLayoutURL = prefs.getBool("useLayoutURL");
    this.isDefaultLayout = prefs.getBool("useLayoutURL") && (!prefs.getString("layoutURL") || prefs.getString("layoutURL") == defaultLayout) ? true : false;	//Default table layout.
    this.layoutURL = prefs.getString("layoutURL") ? prefs.getString("layoutURL") : defaultLayout;
    this.scrollBy = prefs.getString("scrollBy");
    this.scrollDirection = prefs.getString("scrollDirection");
    this.duration = prefs.getInt("duration") * 1000;
    this.interactivityTimeout = prefs.getInt("interactivityTimeout") * 1000;
    this.spacing = parseInt(prefs.getInt("spacing") / 2) + "px";
    this.interval = prefs.getInt("interval");
    this.showStale = prefs.getBool("showStale");
    this.condition1 = prefs.getInt("condition1");
    this.condition2 = prefs.getInt("condition2");
    this.condition3 = prefs.getInt("condition3");
    this.condition1Action = prefs.getString("condition1Action");
    this.condition2Action = prefs.getString("condition2Action");
    this.condition3Action = prefs.getString("condition3Action");
    this.rsW = prefs.getInt("rsW");
    this.rsH = prefs.getInt("rsH");
    this.isLoading = true;
    this.isPaused = true;
    this.noDataFound = false;
}
Spreadsheet.prototype.initialize = function() {
    var self = this,
	params = {};
            
    //Same CSS file is used for default table layout and when a custom layout is not used.
    if (!this.useLayoutURL) {
	self.loadCSS("http://c560290.r90.cf2.rackcdn.com/Table/Table.css");
	//self.loadCSS("http://risegadgets.googlecode.com/svn/test/GoogleSpreadsheetCurrent/Table/Table.css");
    }
    
    //Load XML layout.
    if (this.useLayoutURL) {
	params[gadgets.io.RequestParameters.CONTENT_TYPE] = gadgets.io.ContentType.DOM;
	gadgets.io.makeRequest(this.layoutURL, function(obj) {
	    var data = obj.data;
	    var index = 0;	    
	    
	    if (data.getElementsByTagName("Style").length > 0) {
		self.loadCSS(data.getElementsByTagName("Style")[0].getAttribute("url"));
	    }
	
	    //Save the layout.
	    self.layout = data.getElementsByTagName("Layout")[0].childNodes[1].nodeValue;
	    self.getData();
	}, params);
    }
    else {
	this.getData();
    }
}
//Load CSS file.
Spreadsheet.prototype.loadCSS = function(url) {
    var link = $("<link>");	 
    
    link.attr({
	type: "text/css",
	rel: "stylesheet",
	href: url
    });
    $("head").append(link);
}
Spreadsheet.prototype.getData = function(url) {
    var self = this;
    
    if (url) {
	this.url = url;
    }
    
    if (this.isHorizontal()) {
	this.initHorizontalScroll();
    }
    else {
	if (this.useLayoutURL) {
	    var settings = {
		url: this.url,
		refreshInterval: this.interval,
		callback: function(data) {
		    self.pause();
		    self.data = data;
		    self.showCustomLayout.call(self);
		}
	    };    
	    getRawData(settings);
	}
	else {
	    var settings = {
		url: this.url,
		refreshInterval: this.interval,
		callback: function(data) {
		    self.pause();
		    self.data = data;
		    self.showDefaultLayout.call(self);
		}
	    };
	    getRawData(settings);
	}
    }
}
Spreadsheet.prototype.showCustomLayout = function() {
    var $repeat;

    if (!this.checkForNoData()) {
	this.cols = [];
	this.imagesLoaded = 0;
	
	$("#container").empty();
	$("#container").append(this.layout);
	$repeat = $(".repeat");
	
	$("th, td").css({
	    "padding-top": this.spacing,
	    "padding-bottom": this.spacing
	});
	
	for (var col = 0; col < this.data.getNumberOfColumns(); col++) {
	    this.cols.push(this.data.getColumnId(col));
	}
	
	//Calculate number of images that need to be loaded.
	this.imageTotal = ($(".image").length * this.data.getNumberOfRows()) + ($(".qrCode").length * this.data.getNumberOfRows());
	
	for (var row = 0, numRows = this.data.getNumberOfRows(); row < numRows; row++) {
	    if (row > 0) {
		$repeat.parent().append($repeat.clone());
	    }
	    
	    for (var col = 0, numCols = this.cols.length; col < numCols; col++) {
		var $cell = $("." + this.cols[col] + ":last");		
		
		if ($cell) {
		    if ($cell.hasClass("image")) {	//Show an image.
			this.loadImage(this.data.getValue(row, col), $cell);   			
		    }
		    else if ($cell.hasClass("qrCode")) {	//Generate QR code.
			if (this.data.getValue(row, col)) {
			    this.loadImage("https://chart.googleapis.com/chart?cht=qr&chs=100x100&chld=H|0&chl=" + encodeURIComponent(this.data.getValue(row, col)), $cell);
			}
			else {
			    this.imageTotal--;
			}
		    }
		    else {
			if (this.data.getColumnType(col) == "date") {
			    $cell.text(this.data.getFormattedValue(row, col));
			}
			else {	//Text
			    $cell.text(this.data.getFormattedValue(row, col));
			}
		    }
		}
	    }
	}
	
	this.handleConditions();
    }
}
Spreadsheet.prototype.handleConditions = function() {
    if (this.interval > 0) {
	if (!this.conditions) {	//On initial load.
	    this.conditions = {};
	}
	
	if (this.condition1 != -1) {
	    if (this.condition1Action == "valuePositive" || this.condition1Action == "valueNegative") {
		this.checkSigns(this.condition1Action, this.condition1);
	    }
	    else {
		if (this.conditions.condition1) {
		    this.checkConditions(this.conditions.condition1, this.condition1Action, this.condition1);
		}
	    }
	}
	
	if (this.condition2 != -1) {
	    if (this.condition2Action == "valuePositive" || this.condition2Action == "valueNegative") {
		this.checkSigns(this.condition2Action, this.condition2);
	    }
	    else {
		if (this.conditions.condition2) {
		    this.checkConditions(this.conditions.condition2, this.condition2Action, this.condition2);
		}
	    }
	}
	
	if (this.condition3 != -1) {
	    if (this.condition3Action == "valuePositive" || this.condition3Action == "valueNegative") {
		this.checkSigns(this.condition3Action, this.condition3);
	    }
	    else {
		if (this.conditions.condition3) {
		    this.checkConditions(this.conditions.condition3, this.condition3Action, this.condition3);
		}
	    }
	}
    
	this.saveConditions();
    }
    
    if (this.imageTotal == 0) {
	this.configureScrolling();
	this.isLoading = false;
	readyEvent();
    }
}
Spreadsheet.prototype.checkSigns = function(action, colIndex) {
    for (var row = 0, numRows = this.data.getNumberOfRows(); row < numRows; row++) {
	var current = this.data.getValue(row, colIndex);

	if (isNaN(current)) {
	    current = current.replace(/[^0-9\.-]+/g,"");
	    current = parseFloat(current);
	}

	if (!isNaN(current)) {
	    var $cell = $("." + this.cols[colIndex]).eq(row);

	    if (action == "valuePositive") {
		if (current >= 0) {
		    $cell.addClass("valuePositivePositive");
		}
		else {
		    $cell.addClass("valuePositiveNegative");
		}
	    }
	    else {
		if (current < 0) {
		    $cell.addClass("valueNegativeNegative");
		}
		else {
		    $cell.addClass("valueNegativePositive");
		}
	    }
	}
    }
}
//Compare new values to old one's.
Spreadsheet.prototype.checkConditions = function(condition, action, colIndex) {
    for (var row = 0, numRows = this.data.getNumberOfRows(); row < numRows; row++) {
	var current = this.data.getValue(row, colIndex);
	var previous = condition[row];

	if (isNaN(current)) {
	    current = current.replace(/[^0-9\.-]+/g,"");
	    current = parseFloat(current);
	}
	
	if (isNaN(previous)) {
	    previous = previous.replace(/[^0-9\.-]+/g,"");
	    previous = parseFloat(previous);
	}	

	//The data type of a column can still be a number even if there is string data in it.
	//To be sure, let's check that the values we are comparing are numbers.
	if (current != previous && !isNaN(current) && !isNaN(previous)) {
	    var $cell = $("." + this.cols[colIndex]).eq(row);
	    
	    if (action == "changeUp") {
		if (current > previous) {
		    $cell.addClass("changeUpIncrease");
		}
		else {
		    $cell.addClass("changeUpDecrease");
		}
	    }
	    else {
		if (current < previous) {
		    $cell.addClass("changeDownDecrease");
		}
		else {
		    $cell.addClass("changeDownIncrease");
		}
	    }
	}
    }
}
Spreadsheet.prototype.saveConditions = function() {
    if (this.condition1 != -1) {
	this.conditions.condition1 = [];
	this.saveCondition(this.conditions.condition1, this.condition1);
    }
    
    if (this.condition2 != -1) {
	this.conditions.condition2 = [];
	this.saveCondition(this.conditions.condition2, this.condition2);
    }
    
    if (this.condition3 != -1) {
	this.conditions.condition3 = [];
	this.saveCondition(this.conditions.condition3, this.condition3);
    }
}
//Store the current values so they can be compared to new values on a refresh.
Spreadsheet.prototype.saveCondition = function(condition, colIndex) {
    for (var row = 0, numRows = this.data.getNumberOfRows(); row < numRows; row++) {
	condition.push(this.data.getValue(row, colIndex));
    }				
}
Spreadsheet.prototype.loadImage = function(url, $cell) {
    var self = this,
	img = new Image();
    
    img.onload = function () {
	$cell.append(this);
	self.onImageLoaded();
    }
    
    img.onerror = function() {
	self.onImageLoaded();
    }

    img.src = url;
}
Spreadsheet.prototype.onImageLoaded = function() {
    this.imagesLoaded++;
    
    //May have to resize headers after all images have loaded.
    if (this.imagesLoaded == this.imageTotal) {
	this.configureScrolling();
	this.isLoading = false;
	readyEvent();
    }
}
Spreadsheet.prototype.configureScrolling = function() {
    var self = this;
    
    //Auto-scrolling.
    $(".dataTables_scrollBody").infiniteScroll({
	scrollBy: this.scrollBy,
	duration: this.duration,
	direction: this.scrollDirection,
	swipingTimeout: this.interactivityTimeout
    });
    
    if (this.scrollDirection == "down") {
	$(".page").addClass("scrollDown");
    }
}
/* Code to maintain backward compatibility. */
Spreadsheet.prototype.showDefaultLayout = function() {
    if (!this.checkForNoData()) {
	var sortConfig = {
	    "bDestroy": true,
	    "bFilter": false,
	    "bInfo": false,
	    "bLengthChange": false,
	    "bPaginate": false,
	    "bSort": false,
	    "sScrollY": "500px"
	};
	
	this.cols = [];
	
	$("#container").empty();
	$("#container").append("<table id='info' class='page'>");
	
	var totalCols = this.data.getNumberOfColumns();
	
	for (var col = 0; col < totalCols; col++) {
	    this.cols.push(this.data.getColumnId(col));
	}
	
	//Add column headings.
	if (this.data.getNumberOfColumns() > 0) {
	    this.renderHeadings();
	}
	
	//Add rows.
	for (var row = 0, numRows = this.data.getNumberOfRows(); row < numRows; row++) {
	    this.renderRow(this.data.getNumberOfColumns(), row);
	}
	
	$("th, td").css({
	    "padding-top": this.spacing,
	    "padding-bottom": this.spacing
	});
	
	$("#info").dataTable(sortConfig);
	$(".dataTables_scrollBody").height($("#container").outerHeight(true) - $(".dataTables_scrollHead").height());
	
	this.configureScrolling();
	this.handleConditions();
	
	if (this.isLoading) {
	    this.isLoading = false;
	    readyEvent();
	}
	else {
	    $(".dataTables_scrollBody").infiniteScroll.start();
	}
    }
}
Spreadsheet.prototype.hasHeadings = function() {
    var hasHeading = false;
    
    for (var col = 0; col < this.data.getNumberOfColumns(); col++) {
	var label = this.data.getColumnLabel(col);
	
	if ((label != null) && (label != "")) {
	    hasHeading = true;
	    break;
	}
    }
    
    return hasHeading;
}
//Render the column headings.
Spreadsheet.prototype.renderHeadings = function() {
    var $thead = $("<thead>"),
	$tr = $("<tr>"),
	hasHeadings = this.hasHeadings();
    
    for (var col = 0; col < this.data.getNumberOfColumns(); col++) {
	var $th = $("<th>");
	
	if (hasHeadings) {
	    $th.addClass("nowrap heading_font-style");
	    $th.text(this.data.getColumnLabel(col));
	}
	
	$tr.append($th);
    }
    
    $thead.append($tr)
    $("#info").append($thead);
}
//Render each row of data.
Spreadsheet.prototype.renderRow = function(colsCount, row) {
    var $tr = $("<tr class='item'>");
    
    for (var col = 0; col < colsCount; col++) {
	var value = "", style = "";
    
	value = this.data.getFormattedValue(row, col);
	style = this.data.getProperty(row, col, "style");
	
	//Strip out the font-family that holds an incorrect value.
	if (style) {
	    style = style.substring(0, style.indexOf("font-family:"));
	}
	
	this.addCell($tr, value, style, this.cols[col]);
    }

    $("#info").append($tr);
}
Spreadsheet.prototype.addCell = function($tr, value, style, className) {
    var $td = $("<td>");
    
    if (style != "") {
	$td.attr("style", style);
    }
    
    $td.addClass("data_font-style");
    $td.addClass(className);
    $td.text(value); 
    $tr.append($td);
}
Spreadsheet.prototype.checkForNoData = function() {
    //NODATA is returned by a DDE data source when the DDE application (e.g. Excel) is not open.
    if (this.data.getColumnId(0) == "NODATA") {
	if (!this.noDataFound && this.showStale) {
	    this.noDataFound = true;
	    
	    $(".item").contents().each(function(index) {
		$(this).text($(this).text() + "*");
	    });
	}
	
	if (!this.isLoading && $(".dataTables_scrollBody").length > 0) {
	    $(".dataTables_scrollBody").infiniteScroll.start();
	}
	
	return true;
    }
    else {
	this.noDataFound = false;
	return false;
    }
}
Spreadsheet.prototype.isHorizontal = function() {
    return this.scrollDirection == "rtl" || this.scrollDirection == "ltr";
}
Spreadsheet.prototype.initHorizontalScroll = function() {
    var self = this,
	settings = {
	url: this.url,
	refreshInterval: this.interval,
	callback: function(result) {
	    var data = [];
	    
	    $("#container").remove();
	    
	    for (var row = 0; row < result.getNumberOfRows(); row++) {
		var rowData = "", item = [];
		
		for (var col = 0; col < result.getNumberOfColumns(); col++) {
		    if (rowData == "") {
			rowData = result.getValue(row, col);
		    }
		    else {
			rowData = rowData + " " + result.getValue(row, col);
		    }
		}
		
		item.push({
		    type: "text",
		    value: rowData,
		    fontRule: document.styleSheets[1].cssRules[3].cssText
		});
		
		data.push(item);
	    }
	    
	    //Object already exists, which means this is a data refresh.
	    if (self.horizontalScroll) {
		self.horizontalScroll.refresh(data);
	    }
	    else {
		self.horizontalScroll = new HorizontalScroll({
		    width: self.prefs.getString("rsW"),
		    height: self.prefs.getString("rsH"),
		    scrollBy: self.prefs.getString("scrollBy"),
		    scrollDirection: self.prefs.getString("scrollDirection"),
		    duration: self.prefs.getInt("duration") * 1000,
		    spacing: self.prefs.getInt("spacing")
		}, data);
		
		$(self.horizontalScroll).bind({
		    done: function() {
			doneEvent();
		    }
		});
		
		//Need to add a delay that allows sufficient time for the custom font to be loaded by the div tag.
		//If the font is not already loaded, the canvas can't use it.
		setTimeout(function() {
		    self.horizontalScroll.initialize();
		}, 1000);
	    }
	}
    };
    
    getRawData(settings);
}
Spreadsheet.prototype.play = function() {
    if (!this.isHorizontal() && $(".dataTables_scrollBody").length > 0) {
	$(".dataTables_scrollBody").infiniteScroll.start();
    }
    
    if (this.isPaused) {
	this.isPaused = false;
	
	if (this.isHorizontal()) {
	    this.horizontalScroll.tick();
	}
    }
}
Spreadsheet.prototype.pause = function() {
    this.isPaused = true;
    
    if (this.isHorizontal()) {
	this.horizontalScroll.pause();
    }
    else if (!this.isLoading && $(".dataTables_scrollBody").length > 0) {
	$(".dataTables_scrollBody").infiniteScroll.pause();
    }
}